org 10000h
    jmp Label_Start

%include "fat12.inc"

; 内核加载到0x0:0x100000地址，1M地址
BaseOfKernelFile        equ         0x0
OffsetOfKernelFile      equ         0x100000

; 内核临时存放地址，BIOS中断服务在实模式下只支持1MB一下物理地址空间寻址，因此需要中转一下
BaseTmpOfKernelAddr     equ         0x0
OffsetTmpOfKernelFile   equ         0x7e00

; 内核加载完成后，该地址不再使用，用来存放内存相关信息
MemoryStructBufferAddr  equ         0x7e00

[SECTION gdt]
LABEL_GDT:          dd  0,0                         ; 第一项必须为null
LABEL_DESC_CODE32:  dd  0x0000FFFF,0x00CF9A00       ; 第二项为代码段
LABEL_DESC_DATA32:  dd  0x0000FFFF,0x00CF9200       ; 第三项为数据段

GdtLen                  equ         $ - LABEL_GDT
GdtPtr                  dw          GdtLen - 1      ; GDT表长度
    dd LABEL_GDT                                    ; GDT表基地址

; 段选择子
SelectorCode32          equ         LABEL_DESC_CODE32 - LABEL_GDT
SelectorData32          equ         LABEL_DESC_DATA32 - LABEL_GDT

[SECTION gdt64]
LABEL_GDT64:        dq  0x0000000000000000          ; 第一项必须为null
LABEL_DESC_CODE64:  dq  0x0020980000000000          ; 第二项为代码段
LABEL_DESC_DATA64:  dq  0x0000920000000000          ; 第三项为数据段

GdtLen64                equ         $ - LABEL_GDT64
GdtPtr64                dw          GdtLen64 - 1
    dd LABEL_GDT64

; 64位段选择子
SelectorCode64          equ         LABEL_DESC_CODE64 - LABEL_GDT64
SelectorData64          equ         LABEL_DESC_DATA64 - LABEL_GDT64

[SECTION .s16]  ; 定义一个名为.s16的段
[BITS 16]       ; 表示指令运行在16位宽下，使用32位宽数据指令时需要在指令前加入前缀0x66，使用32位宽地址指令时需要在指令前加入前缀0x67

Label_Start:
    mov ax, cs
    mov ds, ax
    mov es, ax
    mov ax, 0x00
    mov ss, ax
    mov sp, 0x7c00

; ========== display on screen : Start Loader......
    mov ax, 1301h
    mov bx, 000fh              
    mov dx, 0200h               ; 第2行
    mov cx, 12
    push ax
    mov ax, ds
    mov es, ax
    pop ax
    mov bp, StartLoaderMessage
    int 10h

    ;jmp $

; ========== open address A20
    push ax
    in al, 92h
    or al, 00000010b
    out 92h, al
    pop ax
    
    cli                         ; 开启A20功能后，关闭外部中断

    db 0x66                     ; 使用32位数据指令，需要在指令前加上0x66前缀               
    lgdt [GdtPtr]               ; 加载保护模式结构数据信息

    mov eax, cr0                ; 通过cr0第0位开启保护模式
    or eax, 1
    mov cr0, eax

    mov ax, SelectorData32      ; ax = SelectorData32
    mov fs, ax                  ; fs = SelectorData32
    mov eax, cr0                ; 关闭保护模式，只要不对段寄存器重新赋值，实模式下也会按照保护模式下的寻址方式寻址
    and al, 11111110b
    mov cr0, eax

    sti                         ; 开启中断，前面开启再关闭保护模式，是为了让fs段寄存器拥有在实模式下寻址能力超过1MB

    ;jmp $                       ; 死循环，程序执行到这里，可以在bochs中按下Ctrl+C，查看fs段寄存器信息

; ========== reset floppy
    xor ah, ah
    xor dl, dl
    int 13h

; ========== search kernel.bin
    mov word [SectorNo], SectorNumOfRootDirStart    ; 根目录起始扇区号存到SectorNo变量中                                                                               
Label_Search_In_Root_Dir_Begin:
    cmp word [RootDirSizeForLoop], 0    ; 判断根目录遍历剩余扇区数
    jz Label_No_LoaderBin               ; 如果没有扇区要遍历了，跳转到没有找到Loader.bin的处理流程
    dec word [RootDirSizeForLoop]       ; RootDirSizeForLoop变量减1
    mov ax, 00h                         ; ax = 0
    mov es, ax                          ; es = 0
    mov bx, 8000h                       ; bx = 0x8000
    mov ax, [SectorNo]                  ; ax = SectorNo，第一个根目录扇区
    mov cl, 1                           ; cl = 1
    call Func_ReadOneSector             ; 根目录扇区加载到0x8000
    mov si, KernelFileName              ; si = KernelFileName的起始地址
    mov di, 8000h                       ; di = 0x8000
    cld                                 ; DF位置0，表示从低地址向高地址读取
    mov dx, 10h                         ; dx = 0x10，记录每个扇区容纳的目录项个数，一个目录项32字节，512/32=16=0x10
Label_Search_For_LoaderBin:
    cmp dx, 0                           ; if (dx == 0)
    jz Label_Goto_Next_Sector_In_Root_Dir    ; 如果dx == 0成立，表示当前扇区搜索完成，跳转Label_Goto_NextSector_In_Root_Dir循环执行下一个扇区的搜索
    dec dx                              ; dx减1
    mov cx, 11                          ; cx = 11，记录文件名长度，总共11字节
Label_Cmp_FileName:
    cmp cx, 0                           ; if (cx == 0)
    jz Label_FileName_Found             ; 如果cx == 0成立，表示找到loader.bin，跳转到Label_FileName_Found
    dec cx                              ; cx减1
    lodsb                               ; 从ds:si读取1个字节数据到al，si加1，依次读取文件名到al
    cmp al, byte [es:di]                ; al中保存文件名，es:di保存从扇区读取的数据
    jz Label_Go_On                      ; 相等跳转到Label_Go_On继续对比
    jmp Label_Different                 ; 不相等跳转到Label_Diferent

Label_Go_On:
    inc di                              ; 前面判断成立，di加1
    jmp Label_Cmp_FileName              ; 跳转到Label_Cmp_FileName循环对比文件名
Label_Different:                        ; 文件名不相同
    and di, 0ffe0h                      ; di = di & 0xffe0，每个目录项32字节，当前目录项没有找到，di & 0xffe0回到目录项的起始地址
    add di, 20h                         ; di = di + 0x20，di指向下一个目录项
    mov si, KernelFileName              ; si重新指向文件名起始地址
    jmp Label_Search_For_LoaderBin      ; 跳转到Label_Search_For_LoaderBin继续匹配文件名
Label_Goto_Next_Sector_In_Root_Dir:
    add word [SectorNo], 1              ; SectorNo变量加1
    jmp Label_Search_In_Root_Dir_Begin  ; 跳转到Label_Search_In_Root_Dir_Begin开始新的扇区搜索

; =========== display on screen: ERROR:No KERNEL Found
Label_No_LoaderBin:                     ; 同前面显示StartBootMessage
    mov ax, 1301h
    mov bx, 008ch
    mov dx, 0300h                       ; 第三行
    mov cx, 21
    push ax
    mov ax, ds
    mov es, ax
    pop ax
    mov bp, NoLoaderMessage
    int 10h
    jmp $

; ========== found kernel.bin name in root director struct
Label_FileName_Found:
    mov ax, RootDirSectors              ; ax记录根目录扇区数
    and di, 0ffe0h                      ; di指向匹配到的目录项首地址
    add di, 01ah                        ; di = di + 0x1a，di指向起始簇号
    mov cx, word [es:di]                ; 读取起始簇号保存到cx
    push cx                             ; cx压栈
    add cx, ax                          ; cx加上根目录扇区数
    add cx, SectorBalance               ; cx加上SectorBalance找到对应数据区扇区号
    mov eax, BaseTmpOfKernelAddr        ; ax保存kernel文件临时保存段地址0x0
    mov es, eax                         ; 文件保存段地址赋值给es
    mov bx, OffsetTmpOfKernelFile       ; bx保存kernel文件临时保存的偏移地址，现在文件保存地址为es:bx
    mov ax, cx                          ; 前面计算的数据区扇区号保存到ax
Label_Go_On_Loading_File:
    ; INT 10h, AH=0Eh功能：在屏幕上显示一个字符
    ;       AL = 待显示的字符
    ;       BL = 前景色
    push ax                             ; ax压栈
    push bx                             ; bx压栈
    ; 显示一个字符.
    mov ah, 0eh                         ; ah = 0x0e
    mov al, '.' 
    mov bl, 0fh 
    int 10h 
    ; 恢复ax,bx
    pop bx
    pop ax

    mov cl, 1                           ; cl = 1，读取一个扇区
    call Func_ReadOneSector             ; 调用Func_ReadOneSector读取一个扇区
    pop ax                              ; 从栈中取出Label_FileName_Found中push cx保存的起始簇号

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;        ; 将kernel从临时地址搬移到真实保存地址
    push cx                             ; 保存现场
    push eax
    push fs
    push edi
    push ds
    push esi

    mov cx, 200h                        ; cx = 0x200，一个扇区512字节
    mov ax, BaseOfKernelFile            ; ax记录kernel保存的段地址
    mov fs, ax                          ; 赋值给fs，前面开启保护模式再关闭，fs获取了超过1MB的寻址能力
    mov edi, dword [OffsetOfKernelFileCount]    ; OffsetOfKernelFileCount记录了移动了多少的kernel数据，[fs:edi]指向kernel接下来迁移的目标地址

    mov ax, BaseTmpOfKernelAddr         ; ax = BaseTmpOfKernelAddr
    mov ds, ax                          ; ds保存kernel临时存放地址的段地址
    mov esi, OffsetTmpOfKernelFile      ; esi保存kernel临时存放地址的偏移地址，[ds:esi]指向kernel临时存放地址

Label_Mov_Kernel:                       ; 开始一个字节一个字节地搬迁加载的kernel数据
    mov al, byte [ds:esi]               ; [ds:esi] => al
    mov byte [fs:edi], al               ; al => [fs:edi]
    inc esi                             ; esi加1
    inc edi                             ; edi加1
    loop Label_Mov_Kernel               ; cx减1，如果cx不为0，循环执行Label_Mov_Kernel
    mov eax, 0x1000                     ; eax = 0x1000
    mov ds, eax                         ; 恢复数据段寄存器ds值为0x1000，接下来赋值的变量数据段地址为0x1000
    mov dword [OffsetOfKernelFileCount], edi    ; 更新变量OffsetOfKernelFileCount的值，保证kernel数据连续

    pop esi                             ; 恢复现场
    pop ds
    pop edi
    pop fs
    pop eax
    pop cx
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;          ; 继续加载kernel下一个扇区数据
    call Func_GetFATEntry               ; 调用Func_GetFATEntry获取下一个FAT表
    cmp ax, 0fffh                       ; 如果取得值为0x0fff表示结束簇
    jz Label_File_Loaded                ; 跳转到Label_File_Loaded，文件加载完成
    push ax                             ; 否则保存簇号，用于下一次查找下一个FAT表
    mov dx, RootDirSectors              ; dx = 根目录扇区数
    add ax, dx                          ; ax = ax + 根目录扇区数
    add ax, SectorBalance               ; ax = ax + SectorBalance获得对应数据区扇区号
    jmp Label_Go_On_Loading_File        ; 跳转到Label_Go_On_Loading_File继续读取文件

Label_File_Loaded:
    ; 一种新的显示字符方法，从内存地址0xb800开始，是一段专门用于显示的内存空间，每个字符占两个字节，低字节保存显示的字符，高字节保存字符属性
    mov ax, 0B800h
    mov gs, ax
    mov ah, 0Fh                         ; 0000: 黑底    1111: 白字
    mov al, 'G'
    mov [gs:((80 * 0 + 39) * 2)], ax    ; 屏幕第0行，第39列

KillMotor:                              ; 内核加载完成后，可以关闭软盘驱动马达，向I/O端口0x3F2写入控制命令
    ; 软盘驱动器控制功能表
    ; bit7          MOT_EN3             控制软驱D马达，1：启动；0：关闭
    ; bit6          MOT_EN2             控制软驱C马达，1：启动；0：关闭
    ; bit5          MOT_EN1             控制软驱B马达，1：启动；0：关闭
    ; bit4          MOT_EN0             控制软驱A马达，1：启动；0：关闭
    ; bit3          DMA_INT             1：允许DMA和中断请求；0：禁止DMA和中断请求
    ; bit2          RESET               1：允许软盘控制器发送控制信息；0：复位软盘驱动器
    ; bit1          DRV_SEL1            2bit一起使用，00~11用于选择软盘驱动器A~D
    ; bit0          DRV_SEL0            
    push dx
    mov dx, 03F2h
    mov al, 0
    out dx, al                          ; 向0x3f2端口写入0，关闭全部软盘驱动器
    pop dx

; ========== get memory address size type
    mov ax, 1301h
    mov bx, 000Fh
    mov dx, 0400h                       ; 第4行
    mov cx, 24
    push ax
    mov ax, ds
    mov es, ax
    pop ax
    mov bp, StartGetMemStructMessage    ; 显示StartGetMemStructMessage
    int 10h

    mov ebx, 0                          ; ebx = 0
    mov ax, 0x00                        ; ax = 0
    mov es, ax                          ; es = 0
    mov di, MemoryStructBufferAddr      ; di = MemoryStructBufferAddr，存放内存信息，结构体信息参考kernel中memory.h中st_mem_E820结构体

Label_Get_Mem_Struct:
    mov eax, 0x0e820                    ; eax = 0xe820, INT 15h功能：获取物理地址空间信息
    mov ecx, 20                         ; 预设返回结果的缓存区结构体长度，字节为单位
    mov edx, 0x534d4150                 ; 字符串"SMAP"
    int 15h                             ; 调用BIOS中断
    jc Label_Get_Mem_Fail               ; CF=1说明操作失败，CF=0说明操作成功，失败跳转到Label_Get_Mem_Fail执行
    add di, 20                          ; es:di保存获取的结构体信息，di = di + 20，为获取下一个结构体信息准备

    cmp ebx, 0                          ; ebx = 0表示第一个结构体信息，说明检测结束，否则为下一个结构体信息
    jne Label_Get_Mem_Struct            ; 跳转到Label_Get_Mem_Struct继续获取下一个结构体信息
    jmp Label_Get_Mem_OK                ; 跳转到Label_Get_Mem_OK完成物理地址空间信息获取

Label_Get_Mem_Fail:
    mov ax, 1301h
    mov bx, 008ch
    mov dx, 0500h                       ; 第5行
    mov cx, 23                          ; 字符串长度
    push ax
    mov ax, ds
    mov es, ax
    pop ax
    mov bp, GetMemStructErrMessage
    int 10h
    jmp $
Label_Get_Mem_OK:
    mov ax, 1301h
    mov bx, 000fh
    mov dx, 0600h                       ; 第6行
    mov cx, 29                          ; 字符串长度
    push ax
    mov ax, ds
    mov es, ax
    pop ax
    mov bp, GetMemStructOKMessage
    int 10h

; ========== get SVGA information
    mov ax, 1301h
    mov bx, 000fh
    mov dx, 0800h                       ; 第8行
    mov cx, 23
    push ax
    mov ax, ds
    mov es, ax
    pop ax
    mov bp, StartGetSVGAVBEInfoMessage
    int 10h

    mov ax, 0x00
    mov es, ax
    mov di, 0x8000
    mov ax, 4f00h
    int 10h
    cmp ax, 004fh
    jz .KO
; ========== Fail
    mov ax, 1301h
    mov bx, 008ch
    mov dx, 0900h                       ; 第9行
    mov cx, 23
    push ax
    mov ax, ds
    mov es, ax
    pop ax
    mov bp, GetSVGAVBEInfoErrMessage
    int 10h
    jmp $

.KO:
    mov ax, 1301h
    mov bx, 000fh
    mov dx, 0a00h                       ; 第10行
    mov cx, 29
    push ax
    mov ax, ds
    mov es, ax
    pop ax
    mov bp, GetSVGAVBEInfoOKMessage
    int 10h

; ========== Get SVGA Mode Info
    mov ax, 1301h
    mov bx, 000fh
    mov dx, 0c00h                       ; 第12行
    mov cx, 24
    push ax
    mov ax, ds
    mov es, ax
    pop ax
    mov bp, StartGetSVGAModeInfoMessage
    int 10h

    mov ax, 0x00
    mov es, ax
    mov si, 0x800e
    mov esi, dword [es:si]
    mov edi, 0x8200

Label_SVGA_Mode_Info_Get:
    mov cx, word [es:esi]
; ========== display SVGA mode information
    push ax
    mov ax, 00h
    mov al, ch
    call Func_DispAL
    
    mov ax, 00h
    mov al, cl
    call Func_DispAL
    pop ax

; ========
    cmp cx, 0ffffh
    jz Label_SVGA_Mode_Info_Finish

    mov ax, 4F01h
    int 10h
    cmp ax, 004fh
    jnz Label_SVGA_Mode_Info_FAIL
    add esi, 2
    add edi, 0x100
    jmp Label_SVGA_Mode_Info_Get

Label_SVGA_Mode_Info_FAIL:
    mov ax, 1301h
    mov bx, 008ch                       ; 文字闪烁
    mov dx, 0d00h                       ; 第13行
    mov cx, 24
    push ax
    mov ax, ds
    mov es, ax
    pop ax
    mov bp, GetSVGAModeInfoErrMessage
    int 10h

Label_Set_SVGA_Mode_VESA_VBE_FAIL:
    jmp $

Label_SVGA_Mode_Info_Finish:
    mov ax, 1301h
    mov bx, 000fh
    mov dx, 0e00h                       ; 第14行
    mov cx, 30
    push ax
    mov ax, ds
    mov es, ax
    pop ax
    mov bp, GetSVGAModeInfoOKMessage
    int 10h


; ========== set the SVGA mode(VESA VBE)
    ; 显示模式
    ; 0x180     1440列900行     地址0xe0000000      32位像素点
    ; 0x143     800列600行      地址0xe0000000      32位像素点
    mov ax, 4f02h
    mov bx, 4180h                       ; mode: 0x180/0x143
    int 10h

    cmp ax, 004fh
    jnz Label_Set_SVGA_Mode_VESA_VBE_FAIL
    
; ========== init IDT GDT goto protect mode
    cli                                 ; 禁用中断
    db 0x66                             ; 前缀，表示当前指令的操作数是32位
    lgdt [GdtPtr]                       ; 设置GDT

    ; db 0x66
    ; lidt [IDT_POINTER]

    mov eax, cr0                        ; eax = cr0
    or eax, 1                           ; eax = eax | 1
    mov cr0, eax                        ; cr0 = eax，设置PE位进入保护模式，后面必须跟一个far jmp
    jmp dword SelectorCode32:GO_TO_TMP_Protect  ; 跳转到32位指令区

[SECTION .s32]
[BITS 32]

GO_TO_TMP_Protect:
    
; ========== go to tmp long mode
    mov ax, 0x10                        ; 设置段寄存器，现在段寄存器中存放的是段选择子，等于SelectorData32
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov ss, ax
    mov esp, 7e00h                      ; 设置栈顶
    call Func_support_long_mode         ; 调用Func_support_long_mode检测系统是否支持长模式
    test eax, eax                       ; tmp = eax & eax
    jz no_support                       ; 如果tmp为0，跳转到no_support执行

; ========== init temporaty page table 0x90000
    mov dword [0x90000], 0x91007        ; 内存分页，在0x90000地址处建立临时分页表
    mov dword [0x90800], 0x91007
    mov dword [0x91000], 0x92007
    mov dword [0x92000], 0x000083
    mov dword [0x92008], 0x200083
    mov dword [0x92010], 0x400083
    mov dword [0x92018], 0x600083
    mov dword [0x92020], 0x800083
    mov dword [0x92028], 0xa00083

; ========== load GDTR
    db 0x66
    lgdt [GdtPtr64]                     ; 设置64位GDT表
    mov ax, 0x10                        ; 重新设置64位段选择子
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    mov esp, 7e00h
;    jmp $                               ; 此处除了cs，其它段寄存器的基地址和段限长失效了
; ========== open PAE
    mov eax, cr4                        ; eax = cr4
    bts eax, 5                          ; eax设置第5位
    mov cr4, eax                        ; cr4 = eax，开启PAE

; ========== load cr3
    mov eax, 0x90000                    ; eax = 0x90000，设置分页表地址
    mov cr3, eax                        ; cr3 = eax

; ========== enable long-mode
    mov ecx, 0c0000080h                 ; 访问MSR寄存器，必须利用ECX传入寄存器地址
    rdmsr                               ; 读取IA32_EFER寄存器
    bts eax, 8                          ; 第8位是LME标志位
    wrmsr                               ; 写回IA32_EFER

; ========== open PE and paging
    mov eax, cr0
    bts eax, 0
    bts eax, 31
    mov cr0, eax
    jmp SelectorCode64:OffsetOfKernelFile   ; 虚拟机中执行b 0x100000，执行的时候会在内核开始处暂停，可以看到系统进入64位长模式了

; ========== test support long mode or not
Func_support_long_mode:
    mov eax, 0x80000000                 ; eax = 0x80000000
    cpuid                               ; 调用cpuid指令确定最高的扩展功能参数
    cmp eax, 0x80000001                 ; 只有扩展功能号大于等于0x80000001，才有可能支持长模式
    setnb al                        
    jb support_long_mode_done
    mov eax, 0x80000001
    cpuid
    bt edx, 29
    setc al
support_long_mode_done:
    movzx eax, al
    ret

; ========== no support
no_support:
    jmp $


[SECTION .s16lib]
[BITS 16]

; =========== read one sector from floppy
    ; INT 13h, AH=02h功能：读取磁盘扇区
    ; AL = 读入的扇区数（必须非0）
    ; CH = 磁道号（柱面号）的低8位
    ; CL = 扇区号1~63（bit0~5），磁道号（柱面号）的高2位（bit6~7，只对硬盘有效）
    ; DH = 磁头号
    ; DL = 驱动器号（如果操作的是硬盘驱动器，bit7必须被置位）
    ; ES:BX = 数据缓冲区
Func_ReadOneSector:
    ; 输入参数：AX = 待读取的磁盘起始扇区号
    ;           CL = 读入的扇区数量
    ;           ES:BX = 目标缓冲区起始地址
    push bp
    mov bp, sp
    sub esp, 2  ; 栈指针向下移动2个字节
    mov byte [bp - 2], cl   ; cl的值保存到esp指向内存中
    push bx
    mov bl, [BPB_SecPerTrk] ; bl保存每磁道扇区数目
    div bl  ; ax / bl, al保存商，ah保存余数
    inc ah  ; 余数+1得到扇区号
    mov cl, ah  ; 扇区号保存到cl
    mov dh, al  ; 商保存到dh
    shr al, 1   ; 商除以2得到磁道号
    mov ch, al  ; 磁道号保存到CH
    and dh, 1   ; 商为奇数磁头号为1，为偶数磁头号为0
    pop bx      ; 恢复bx寄存器的值
    mov dl, [BS_DrvNum] ; 设置驱动器号
Label_Go_On_Reading:
    mov ah, 2   ; 中断功能号
    mov al, byte [bp - 2]   ; 扇区数量读出来放入al
    int 13h     ; 中断读取磁盘
    jc Label_Go_On_Reading  ; 读取成功CF标志被复位，否则重新读取
    add esp, 2  ; 恢复现场，栈指针上移两个字节
    pop bp      ; 恢复bp寄存器
    ret         ; 返回

; =========== get FAT Entry

; 软盘FAT12文件系统分配图
; 数据区    19+RootDirSectors~2879          数据区不但可以保存目录项信息还可以保存文件内的数据
; 根目录区  19~19+RootDirSectors-1          根目录区只能保存目录项信息
; FAT2      10~18                           备份FAT表
; FAT1      1~9                             FAT表
; 引导扇区  0

; FAT12表项说明
; 第一个表项0xFF0，磁盘标示字，低字节与BPB_Media数值保持一致
; 第二个表项0xFFF，第一个簇已经被占用
; 0x000: 可用簇
; 0x002~0xFEF: 已用簇，标识下一个簇簇号
; 0xFF0~0xFF6: 保留簇
; 0xFF7: 坏簇
; 0xFF8~0xFFF: 文件的最后一个簇

Func_GetFATEntry:
    ; 输入ax = FAT表项号
    ; 输出ax = FAT表项号
    push es                         ; 保存es
    push bx                         ; 保存bx
    push ax                         ; 保存ax
    mov ax, 00                      ; ax = 0
    mov es, ax                      ; es = 0
    pop ax                          ; 恢复ax
    mov byte [Odd], 0               ; Odd变量 = 0
    mov bx, 3                       ; bx = 3，一个表项12bit，1.5B，先乘3再除以2得到地址
    mul bx                          ; ax = ax * bx
    mov bx, 2                       ; bx = 2
    div bx                          ; ax = ax / bx，dx保存余数
    cmp dx, 0                       ; 如果余数为0，表示表项起始地址为整数
jz Label_Even                   ; 跳转到Label_Even
    mov byte [Odd], 1               ; Odd变量设置为1
Label_Even:
    xor dx, dx                      ; dx = 0
    mov bx, [BPB_BytesPerSec]       ; bx = 512
    div bx                          ; ax = ax / 512，ax得到扇区号，dx得到扇区偏移地址
    push dx                         ; 保存dx
    mov bx, 8000h                   ; bx = 0x8000
    add ax, SectorNumOfFAT1Start    ; FAT1起始扇区号为1，得到FAT1真正扇区号
    mov cl, 2                       ; cl = 2，读取2个扇区的数据，防止出现跨扇区FAT表项
    call Func_ReadOneSector         ; 调用Func_ReadOneSector读取FAT1的扇区数据

    pop dx                          ; 恢复dx
    add bx, dx                      ; bx加上偏移地址，得到表项FAT地址
    mov ax, [es:bx]                 ; 读取2字节数据
    cmp byte [Odd], 1               ; 如果Odd为1表示FAT起始地址不为整数
    jnz Label_Even_2                ; 不为1，起始地址为整数，跳转到Label_Even_2
    shr ax, 4                       ; 否则，ax = ax >> 4，去掉前面0.5字节
Label_Even_2:
    and ax, 0fffh                   ; 取前面1.5字节数据
    pop bx                          ; 恢复bx
    pop es                          ; 恢复es
    ret                             ; 返回

; ========== display num in al
Func_DispAL:
    ; 输入参数：al = 要转16进制的数据
    ;           DisplayPosition = 显示的位置
    push ecx                        ; 保存现场
    push edx
    push edi

    mov edi, [DisplayPosition]      ; edi = 显示位置
    mov ah, 0Fh                     ; ah = 0xf，字符属性
    mov dl, al                      ; dl = al，保留低4位
    shr al, 4                       ; al = al >> 4，先处理高4位
    mov ecx, 2                      ; ecx = 2，总共两位，需要loop指令再循环一次
.begin:
    and al, 0Fh                     ; al = al & 0xf
    cmp al, 9                       ; al和9比较
    ja .1                           ; 比9大，跳转到.1执行
    add al, '0'                     ; al = al + '0'，得到al数值对应的数字字符
    jmp .2                          ; 跳转到.2执行
.1:
    sub al, 0ah                     ; al = al - 10
    add al, 'A'                     ; al = al + 'A'，得到al数值对应的16进制字符
.2:
    mov [gs:edi], ax                ; al中是显示的字符，ah中是字符属性
    add edi, 2                      ; edi加2，一个字符占两个字节
    mov al, dl                      ; al = dl, 处理低4位
    loop .begin                     ; cx = cx - 1，不为0跳转到.begin处理

    mov [DisplayPosition], edi      ; 更新下一个显示位置

    pop edi                         ; 恢复现场
    pop edx
    pop ecx
    ret

; ========== tmp IDT
IDT:
    times   0x50    dq  0           ; 临时IDT结构，切换过程禁用了中断，无须完整初始化IDT表
IDT_END:

IDT_POINTER:
    dw  IDT_END - IDT - 1
    dd  IDT

; =========== tmp variable
    RootDirSizeForLoop      dw      RootDirSectors
    SectorNo                dw      0
    Odd                     db      0
    OffsetOfKernelFileCount dd      OffsetOfKernelFile
    DisplayPosition         dd      0

; ========== display messages
StartLoaderMessage:         db      "Start Loader"
NoLoaderMessage:            db      "ERROR:No KERNEL Found"
KernelFileName:             db      "KERNEL  BIN",0     ; FAT12文件名占8字节，扩展名占3字节，没有'.'
StartGetMemStructMessage:   db      "Start Get Memory Struct."
GetMemStructErrMessage:     db      "Get Memory Struct Error"
GetMemStructOKMessage:      db      "Get Memory Struct Successful!"

StartGetSVGAVBEInfoMessage: db  "Start Get SVGA VBE Info"
GetSVGAVBEInfoErrMessage:   db  "Get SVGA VBE Info ERROR"
GetSVGAVBEInfoOKMessage:    db  "Get SVGA VBE Info SUCCESSFUL!"

StartGetSVGAModeInfoMessage:    db  "Start Get SVGA Mode Info"
GetSVGAModeInfoErrMessage:  db  "Get SVGA Mode Info ERROR"
GetSVGAModeInfoOKMessage:   db  "Get SVGA Mode Info SUCCESSFUL!"
